package cz.muni.fi.pb138.avradiation.parser;

import au.com.bytecode.opencsv.CSVReader;
import cz.muni.fi.pb138.avradiation.Sensor.Sensor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Specialization of <code>Parser</code> interface, which parses CSV files with
 * measured radiation data
 * 
 * @author Andrej Kuročenko <kurochenko@gmail.com>
 */
public class CSVParser implements Parser {
    /** String sequence which indicates data start */
    private static final String DATA_START_SEQUENCE = "datum/cas";
    /** Default sensor location */
    private static final String DEFAULT_LOCATION = "Not Specified";
    /** date format for measure time from CSV files */
    private static final String MDATE_FORMAT = "dd.MM.yyyy H:mm:ss";
    /** CSV file separator */
    private static final char SEPARATOR = ';';
    /** Value of <code>dataLineCounter</code> when data weren't reached yet*/
    private static final int FALSE_DATA_LINE = -1;
    /** Relative line position on which something is located. Line is 
     * counted from data start (<code>dataLineCounter</code>, not from file 
     * beginning */
    private static final int SENSOR_UNKNOWN_VAL_LINE = 0;
    /** Relative line position on which measured units are located. Line is 
     * counted from data start (<code> dataLineCounter</code>, not from file 
     * beginning */
    private static final int SENSOR_UNIT_LINE = 1;
    /** Counter of lines on which data are located */
    private int dataLineCounter = FALSE_DATA_LINE;
    /** Location of sensors */
    private String location = DEFAULT_LOCATION;
    /** List of sensors parsed from CSV file */
    List<Sensor> sensors = new ArrayList<Sensor>();
    
    private static final Logger logger =
            Logger.getLogger(CSVParser.class.getName());

    /**
     * Resets class variables
     */
    private void resetSettings() {
        dataLineCounter = FALSE_DATA_LINE;
        location = DEFAULT_LOCATION;
        sensors.clear();
    }

    @Override
    public Collection<Sensor> parse(String pathToCSV) {
        if (pathToCSV == null) {
            throw new IllegalArgumentException("Path to CSV file is null.");
        }

        File csvPath =  new File(pathToCSV);
        List<Sensor> result = new ArrayList<Sensor>();
        
        if (csvPath.isDirectory()) {
            for (File csvFile : csvPath.listFiles(new CSVFileNameFilter())) {
                result.addAll(parse(csvFile));
                resetSettings();
            }
        } else {
            result.addAll(parse(csvPath));
        }
        
        return result;
    }

    @Override
    public Collection<Sensor> parse(File CSVFile) {
        if (CSVFile == null) {
            throw new IllegalArgumentException("CSV file is null.");
        }
        
        return CSVFile.isDirectory() 
                ? parseDirectory(CSVFile) 
                : parseFile(CSVFile);
    }
    
    /**
     * Parse all CSV files in directory <code>dir</code>
     * 
     * @param dir 
     * @return collection of parsed sensors
     */
    private Collection<Sensor> parseDirectory(File dir) {
        if (dir == null) {
            throw new IllegalArgumentException("Parameter [dir] is null.");
        }
        if (!dir.canRead()) {
            throw new IllegalArgumentException("Can't read directory " 
                    + dir.getName());
        }
        
        List<Sensor> result = new ArrayList<Sensor>();
        
        for (File csvFile : dir.listFiles(new CSVFileNameFilter())) {
            result.addAll(parse(csvFile));
            resetSettings();
        }
        
        return result;        
    }
    
    /**
     * Parses sensors from CSV file
     * 
     * @param file CSV file
     * @return collection of parsed sensors
     */
    private Collection<Sensor> parseFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("Parameter [file] is null.");
        }
        if (!file.canRead()) {
            throw new IllegalArgumentException("Can't read file " 
                    + file.getName());
        }
        
        InputStream fis = null;
        Collection<Sensor> result = null;

        try {
            location = parseLocation(file.getName());
            result = parse(new FileInputStream(file));
        } catch (FileNotFoundException ex) {
            logger.log(Level.SEVERE,
                    "CSV file not found or bad permissions. ", ex);
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException ex) {
                logger.log(Level.SEVERE, "Error when closing CSV file input "
                        + "stream. ", ex);
            }
        }
        
        return result;
    }
    
    @Override
    public Collection<Sensor> parse(InputStream is, String location) {
        if (location == null) {
            throw new IllegalArgumentException("Argument 'location' is null");
        }
        
        return parse(is);
    }

    @Override
    public Collection<Sensor> parse(InputStream is) {
        if (is == null) {
            throw new IllegalArgumentException("Input stream is null");
        }

        CSVReader reader = null;
        String[] line = null;

        try {
            reader = new CSVReader(new InputStreamReader(is), SEPARATOR);

            while ((line = reader.readNext()) != null) {
                if (line.length == 1) {
                    continue;
                }

                switch (dataLineCounter) {
                    case FALSE_DATA_LINE:
                        if (DATA_START_SEQUENCE.equals(line[0])) {
                            createSensors(line);
                            dataLineCounter++;
                        }
                        break;
                    case SENSOR_UNKNOWN_VAL_LINE:
                        dataLineCounter++;
                        break;
                    case SENSOR_UNIT_LINE:
                        setSensorUnit(line);
                        dataLineCounter++;
                        break;
                    default:
                        setMeasuredValues(line);
                        dataLineCounter++;
                        break;
                }
            }

            if (dataLineCounter < (SENSOR_UNIT_LINE + 2)) {
                logger.log(Level.SEVERE, "CSV File doesn't contain all "
                        + "required values.");
                throw new CSVFileNotValidException("CSV File doesn't contain all "
                        + "required values.");
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "Error when reading CSV file. ", ex);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException ex) {
                logger.log(Level.SEVERE, "Error when closing CSV reader. ", ex);
            }
        }

        return sensors;
    }

    /**
     * Creates sensors with names according to values in string array
     * 
     * @param names array of strings
     */
    private void createSensors(String[] names) {
        if (names == null) {
            throw new IllegalArgumentException("Sensor names are null.");
        }

        for (int i = 1; i < names.length - 1; i++) {
            if (names[i].trim().isEmpty()) {
                logger.log(Level.SEVERE, "Empty sensor name at column #{0}.", i);
                throw new CSVFileNotValidException("Empty sensor name at column #"
                        + i + ".");
            }
            Sensor sensor = new Sensor();
            sensor.setName(names[i]);
            sensor.setLocation(location);
            sensors.add(sensor);
        }
    }

    /**
     * Sets sensors units according to array of strings
     * 
     * @param units 
     */
    private void setSensorUnit(String[] units) {
        if (units == null) {
            throw new IllegalArgumentException("Sensor measure units are null.");
        }

        if ((units.length - 2) > sensors.size()) {
            logger.log(Level.SEVERE, "Count of sensor units doesn't "
                    + "correspondent to count of sensors.");
            throw new CSVFileNotValidException("Count of sensor units doesn't "
                    + "correspondent to count of sensors.");
        }

        for (int i = 1; i < units.length - 1; i++) {
            if (units[i].trim().isEmpty()) {
                logger.log(Level.SEVERE, "Empty unit value at column #{0}.", i);
                throw new CSVFileNotValidException("Empty unit value at column #" 
                        + i + ".");
            }
            sensors.get(i - 1).setUnit(units[i]);
        }
    }

    /**
     * Sets measure value to sensor according to array of strings
     * 
     * @param values 
     */
    private void setMeasuredValues(String[] values) {
        if (values == null) {
            throw new IllegalArgumentException("Measured values are null.");
        }

        if ((values.length - 2) > sensors.size()) {
            logger.log(Level.SEVERE, "Count of measured values doesn't "
                    + "correspondent to count of sensors.");
            throw new CSVFileNotValidException("Count of measured values doesn't"
                    + " correspondent to count of sensors.");
        }

        for (int i = 1; i < values.length - 1; i++) {
            sensors.get(i - 1).addMeasuredValue(parseTimestamp(values[0]),
                    parseMeasuredValue(values[i]));
        }
    }

    /**
     * Parses date from CSV to milliseconds
     * 
     * @param timestamp
     * @return milliseconds from 1970
     */
    public static long parseTimestamp(String timestamp) {
        if (timestamp == null) {
            throw new IllegalArgumentException("Parameter [timestamp] is null");
        }

        Date date = null;

        SimpleDateFormat sdf = new SimpleDateFormat(MDATE_FORMAT);
        try {
            date = sdf.parse(timestamp);
        } catch (ParseException ex) {
            logger.log(Level.SEVERE, "Bad measure time format: "
                    + timestamp + ".", ex);
            throw new CSVFileNotValidException("Bad measure time format: "
                    + timestamp + ".");
        }
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.getTimeInMillis();
    }

    /**
     * Parses measure value do BigDecimal
     * 
     * @param value string integer value
     * @return null if <code>value</code> is empty. BigDecimal representation 
     * of number otherwise
     */
    public static BigDecimal parseMeasuredValue(String value) {
        if (value == null) {
            throw new IllegalArgumentException("Measured value is null.");
        }

        return value.trim().isEmpty() ? null : new BigDecimal(value);
    }

    /**
     * Removes suffix from filename and creates location name
     * 
     * @param filename
     * @return location name without file suffix at the end
     */
    private String parseLocation(String filename) {
        if (filename == null) {
            throw new IllegalArgumentException("Parameter [filename] is null.");
        }

        return filename.endsWith(CSVFileNameFilter.CSV_FILE_SUFFIX) 
                ? filename.substring(0, filename.length() - CSVFileNameFilter.CSV_FILE_SUFFIX.length()) 
                : filename;
    }
}
